package be.rivendale.geometry;

import be.rivendale.ghetto.RayBoxIntersectionGhetto;
import be.rivendale.mathematics.Point;
import be.rivendale.mathematics.Ray;
import org.apache.commons.lang.Validate;

/**
 * Represents an axis aligned bounding box.
 * An axis aligned bounding box is 3D cuboid that is fully aligned with the coordinate system's axes.
 * This means that it's parallel to the X, Y and Z axis.
 * <p>These axis aligned bounding boxes are used for many purposes, many of which involve early intersection testing
 * of some objects that lie within the bounds of this box. Intersection tests with an axis aligned bounding box is a cheap operation
 * compared to more complex geometric objects such as triangle, oriented bounding boxes, etc...</p>
 */
public class AxisAlignedBoundingBox {
	/**
	 * The corner of the box with the smallest X, Y and Z coordinates.
	 * Since the box is aligned with the axes, only two points are required to define it.
	 * @see #minimumBound
	 */
	private Point minimumBound;

	/**
	 * The corner of the box with the largest X, Y and Z coordinates.
	 * Since the box is aligned with the axes, only two points are required to define it.
	 * @see #minimumBound
	 */
	private Point maximumBound;

	/**
	 * Creates a new axis aligned bounding box given it's two extreme corners.
	 * <p>The two points must not be null, and must not be equal (the second point must be larger then or equal to the first one)</p>
	 * @param minimumBound The corner of the box to construct with the smallest X, Y and Z values.
	 * @param maximumBound The corner of the box to construct with the largest X, Y and Z values.
	 */
	public AxisAlignedBoundingBox(Point minimumBound, Point maximumBound) {
		validateConstructorParameters(minimumBound, maximumBound);
		this.minimumBound = minimumBound;
		this.maximumBound = maximumBound;
	}

	/**
	 * Performs validation of the constructor parameters to see if the construction of this object is based on parameters
	 * that make sense.
	 * This way an instantiated object is always fully correct and can never trigger corruption in the application.
	 * The following conditions are validated:
	 * <ul><li>The minimum and maximum bounds must not be null</li>
	 * <li>The minimum and maximum bounds must not be equal (this could never be a point cuboid)</li>
	 * <li>The minimum bound must be smaller or equal to the maximum bound
	 * (equal to means that this could be a rectangle in stead of a cuboid)</li></ul>
	 * @param minimumBound The The smallest corner of the bounding box.
	 * @param maximumBound The largest corner of the bounding box.                                              
	 */
	private void validateConstructorParameters(Point minimumBound, Point maximumBound) {
		String className = AxisAlignedBoundingBox.class.getSimpleName();
		Validate.notNull(minimumBound, "The minimum bound of an [" + className + "] must not be null");
		Validate.notNull(maximumBound, "The maximum bound of an [" + className + "] must not be null");
		Validate.isTrue(!minimumBound.equals(maximumBound), "The maximum bound and minimum bound of an [" + className + "] must not be equal");
		Validate.isTrue(minimumBound.getX() <= maximumBound.getX()
				&& minimumBound.getY() <= maximumBound.getY()
				&& minimumBound.getZ() <= maximumBound.getZ(), "The minimum bound of a [" + className + "] must be less then or equal to the maximum bound");
	}

	/**
	 * Retrieves the corner of the box with the smallest coordinates.
	 * @return The minimum bound point.
	 */
	public Point getMinimumBound() {
		return minimumBound;
	}

	/**
	 * Retrieves the corner of the box with the largest coordinates.
	 * @return The maximum bound point.
	 */
	public Point getMaximumBound() {
		return maximumBound;
	}

	/**
	 * Checks if this box and the specified ray intersect.
	 * If an intersection occurs, the value of T on the ray is returned where the ray enters the box.
	 * @param ray The ray to test for an intersection with.
	 * @return Null if no intersection exists. The value of T of the ray where the ray enters the box otherwise
	 */
	public Double intersection(Ray ray) {
		return RayBoxIntersectionGhetto.rayBoxIntersection(ray, this);
	}

	/**
	 * Retrieves the width of this bounding box.
	 * The width is the length of the box over the X axis. This starts from the minimumBound and extends to the maximumBound
	 * @return The width of this bounding box.
	 */
	public double getWidth() {
		return getMaximumBound().getX() - getMinimumBound().getX();
	}

	/**
	 * Retrieves the height of this bounding box.
	 * The height is the length of the box over the Y axis. This starts from the minimumBound and extends to the maximumBound
	 * @return The height of this bounding box.
	 */
	public double getHeight() {
		return getMaximumBound().getY() - getMinimumBound().getY();
	}

	/**
	 * Retrieves the depth of this bounding box.
	 * The depth is the length of the box over the Z axis. This starts from the minimumBound and extends to the maximumBound
	 * @return The depth of this bounding box.
	 */
	public double getDepth() {
		return getMaximumBound().getZ() - getMinimumBound().getZ();
	}

	/**
	 * Checks if two axis alinged bounding boxes intersect.
	 * <p><a href="http://en.wikipedia.org/wiki/Bounding_volume">According to wikipedia</a> the intersection can be tested
	 * as follows:<br>
	 * Assume two axis aligned bounding boxes defined by <code>M,N</code> and <code>O,P</code>.
	 * The two boxes do not intersect if <code>(Mx>Px) or (Ox>Nx) or (My>Py) or (Oy>Ny) or (Mz>Pz) or (Oz>Nz)</code>
	 * @param box The second box to test for an intersection with (the first box is this).
	 * @return true if the boxes intersect, false if they do not intersect.
	 */
	public boolean intersects(AxisAlignedBoundingBox box) {
		return !(this.getMinimumBound().getX() > box.getMaximumBound().getX()
				|| box.getMinimumBound().getX() > this.getMaximumBound().getX()
				|| this.getMinimumBound().getY() > box.getMaximumBound().getY()
				|| box.getMinimumBound().getY() > this.getMaximumBound().getY()
				|| this.getMinimumBound().getZ() > box.getMaximumBound().getZ()
				|| box.getMinimumBound().getZ() > this.getMaximumBound().getZ());
	}
}
